/*
 * Copyright (c) Facebook, Inc. and its affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

package com.facebook.stetho.inspector.elements.android;

import com.facebook.stetho.common.LogUtil;
import com.facebook.stetho.common.Util;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;

/**
 * Tries to arbitrarily invoke single argument methods by name on an object instance by trying out
 * different argument types.
 */
public class MethodInvoker {

  private static final List<TypedMethodInvoker<?>> invokers = Arrays.asList(
      new StringMethodInvoker(), new CharSequenceMethodInvoker(), new IntegerMethodInvoker(),
      new FloatMethodInvoker(), new BooleanMethodInvoker());

  /**
   * Tries to invoke a method on receiver with a single argument by trying out different types
   * for arg until it finds one that matches (or not). No exceptions are thrown on failure.
   *
   * @param methodName The method name to be invoked
   * @param argument The single argument to be provided to the method
   */
  public void invoke(Object receiver, String methodName, String argument) {
    Util.throwIfNull(receiver, methodName, argument);
    int size = invokers.size();
    for (int i = 0; i < size; ++i) {
      final TypedMethodInvoker<?> invoker = invokers.get(i);
      if (invoker.invoke(receiver, methodName, argument)) {
        return;
      }
    }
    LogUtil.w("Method with name " + methodName +
              " not found for any of the MethodInvoker supported argument types.");
  }

  private static abstract class TypedMethodInvoker<T> {
    private final Class<T> mArgType;

    TypedMethodInvoker(Class<T> argType) {
      mArgType = argType;
    }

    boolean invoke(Object receiver, String methodName, String argument) {
      try {
        Method method = receiver.getClass().getMethod(methodName, mArgType);
        method.invoke(receiver, convertArgument(argument));
        return true;
      } catch (NoSuchMethodException ignored) {
        // ignore
      } catch (InvocationTargetException e) {
        LogUtil.w("InvocationTargetException: " + e.getMessage());
      } catch (IllegalAccessException e) {
        LogUtil.w("IllegalAccessException: " + e.getMessage());
      } catch (IllegalArgumentException e) {
        LogUtil.w("IllegalArgumentException: " + e.getMessage());
      }
      return false;
    }

    abstract T convertArgument(String argument);
  }

  private static class StringMethodInvoker extends TypedMethodInvoker<String> {
    StringMethodInvoker() {
      super(String.class);
    }

    @Override
    String convertArgument(String argument) {
      return argument;
    }
  }

  private static class CharSequenceMethodInvoker extends TypedMethodInvoker<CharSequence> {
    CharSequenceMethodInvoker() {
      super(CharSequence.class);
    }

    @Override
    CharSequence convertArgument(String argument) {
      return argument;
    }
  }

  private static class IntegerMethodInvoker extends TypedMethodInvoker<Integer> {
    IntegerMethodInvoker() {
      super(int.class);
    }

    @Override
    Integer convertArgument(String argument) {
      return Integer.parseInt(argument);
    }
  }

  private static class FloatMethodInvoker extends TypedMethodInvoker<Float> {
    FloatMethodInvoker() {
      super(float.class);
    }

    @Override
    Float convertArgument(String argument) {
      return Float.parseFloat(argument);
    }
  }

  private static class BooleanMethodInvoker extends TypedMethodInvoker<Boolean> {
    BooleanMethodInvoker() {
      super(boolean.class);
    }

    @Override
    Boolean convertArgument(String argument) {
      return Boolean.parseBoolean(argument);
    }
  }
}
